package com.masonluo.mlonlinejudge.acl.proxy;

import com.masonluo.mlonlinejudge.acl.annotations.RequireAuthentication;
import com.masonluo.mlonlinejudge.acl.annotations.RequireRoles;
import com.masonluo.mlonlinejudge.acl.core.AuthenticateFilterChain;
import com.masonluo.mlonlinejudge.acl.core.RoleAccessor;
import com.masonluo.mlonlinejudge.acl.core.filter.FilterResult;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

/**
 * 实现了{@link MethodInterceptor}，在这处理代理的逻辑
 *
 * @author masonluo
 * @date 2021/5/5 8:17 下午
 */
public class AclMethodInterceptor implements MethodInterceptor {

    private final ProxyBean proxyBean;

    public AclMethodInterceptor(ProxyBean proxyBean) {
        this.proxyBean = proxyBean;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        ProxyBean.MethodWrapper beforeMethod = proxyBean.get(method);
        // 无需代理
        if (beforeMethod == null) {
            return methodProxy.invoke(proxyBean.getOrigin(), objects);
        }
        FilterResult beforeRet = invokeBeforeMethod(method, beforeMethod);
        if (beforeRet.isSuccess()) {
            return doInvokeMethod(proxyBean, o, method, objects, methodProxy);
        }

        return accessControlFailReturn(proxyBean, beforeRet);
    }

    private Object accessControlFailReturn(ProxyBean proxyBean, FilterResult result) throws InvocationTargetException, IllegalAccessException {
        return proxyBean.getAfterInterceptReturnMethod().invoke(new Object[]{result});
    }

    private Object doInvokeMethod(ProxyBean proxyBean, Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object origin = proxyBean.getOrigin();
        return methodProxy.invoke(origin, objects);
    }

    private FilterResult invokeBeforeMethod(Method method, ProxyBean.MethodWrapper beforeMethod) {
        if (beforeMethod.getExecuteObj() instanceof AuthenticateFilterChain) {
            throw new IllegalArgumentException("The before method container should be the [" + AuthenticateFilterChain.class + "] class");
        }
        Object[] args = obtainArgs(method);
        try {
            Object ret = beforeMethod.invoke(args);
            return (FilterResult) ret;
        } catch (InvocationTargetException | IllegalAccessException e) {
            return FilterResult.error("Authenticate method invoke error, please check the permission, err -> " + e.getMessage());
        }
    }

    private Object[] obtainArgs(Method method) {
        boolean requireRolesAnnotatedMethod = isRequireRolesAnnotatedMethod(method, proxyBean.getOrigin());
        return requireRolesAnnotatedMethod ?
                obtainRequireRolesArgs(method, proxyBean) : obtainRequireAuthenticateArgs(method, proxyBean);
    }

    private Object[] obtainRequireAuthenticateArgs(Method method, ProxyBean proxyBean) {
        return new Object[]{proxyBean.getMetadataAchiever().obtainToken()};
    }

    private Object[] obtainRequireRolesArgs(Method method, ProxyBean proxyBean) {
        Class<?> clazz = proxyBean.getOrigin().getClass();
        List<RoleAccessor> container = new ArrayList<>();
        RoleAccessor roleAccessor = obtainGlobalRoleAccessorIfExist(proxyBean.getOrigin());
        if (roleAccessor != null) {
            container.add(roleAccessor);
        }
        roleAccessor = obtainMethodRoleAccessorIfExist(method);
        if (roleAccessor != null) {
            container.add(roleAccessor);
        }
        return new Object[]{proxyBean.getMetadataAchiever().obtainToken(), container};
    }

    private RoleAccessor obtainMethodRoleAccessorIfExist(Method method) {
        RequireRoles anno = method.getAnnotation(RequireRoles.class);
        if (anno == null) {
            return null;
        }
        return buildRolesAccessor(anno);
    }

    private RoleAccessor obtainGlobalRoleAccessorIfExist(Object origin) {
        RequireRoles anno = origin.getClass().getAnnotation(RequireRoles.class);
        if (anno == null) {
            return null;
        }
        return buildRolesAccessor(anno);
    }

    private boolean isRequireRolesAnnotatedMethod(Method method, Object origin) {
        Class<?> clazz = origin.getClass();
        if (clazz.isAnnotationPresent(RequireRoles.class) || clazz.isAnnotationPresent(RequireAuthentication.class)) {
            return judgeByOriginObject(method, origin);
        }
        return judgeByMethod(method);
    }

    private boolean judgeByOriginObject(Method method, Object origin) {
        Class<?> clazz = origin.getClass();
        Annotation annotation = clazz.getAnnotation(RequireRoles.class) != null ?
                clazz.getAnnotation(RequireRoles.class) : clazz.getAnnotation(RequireAuthentication.class);
        if (annotation instanceof RequireRoles) {
            return true;
        }
        return method.isAnnotationPresent(RequireRoles.class);
    }

    private boolean judgeByMethod(Method method) {
        return method.isAnnotationPresent(RequireRoles.class);
    }

    private RoleAccessor buildRolesAccessor(RequireRoles roles) {
        RoleAccessor roleAccessor = new RoleAccessor();
        roleAccessor.setRoles(new HashSet<>(Arrays.asList(roles.values())));
        roleAccessor.setModel(roles.model());
        return roleAccessor;
    }
}
